package batch.util;

import java.lang.reflect.Array;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.StringJoiner;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import core.config.Factory;
import core.util.DateUtil;

/**
 * パラメタユーティリティクラス
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class ParameterUtil {

	/**
	 * コンストラクタ
	 */
	private ParameterUtil() {
		throw new AssertionError();
	}

	/**
	 * 値存在確認
	 * @param item 項目名
	 * @param params パラメータマップ
	 * @return 存在する場合 true を返す。
	 */
	public static boolean hasValue(final String item, final Map<String, String[]> params) {
		String[] pattern = params.get(item);
		return pattern != null && 0 < pattern.length && !Objects.toString(pattern[0], "").isEmpty();
	}

	/**
	 * パラメタ文字列配列化
	 *
	 * @param param パラメタ文字列（Key=Valueの空白区切り）
	 * @return Key=Value毎の配列
	 */
	public static String[] toArray(final String param) {
		if (param != null) {
			String str = param.trim();
			if (!str.isEmpty()) {
				String[] ret = split(str);
				for (int i = 0; i < ret.length; i++) {
					ret[i] = unescape(ret[i]);
				}
				return ret;
			}
		}
		return new String[0];
	}

	/**
	 * 分割
	 *
	 * @param str 文字列
	 * @return 分割リスト
	 */
	private static String[] split(final String str) {
		List<String> ret = new ArrayList<>();

		int quot = 0;
		int i = 0;
		int s = 0;
		while (i < str.length()) {
			if (str.codePointAt(i) == '"') {
				if (quot == '"') {
					quot = 0;
				} else if (quot == 0) {
					quot = '"';
				}
			} else if (str.codePointAt(i) == '\'') {
				if (quot == '\'') {
					quot = 0;
				} else if (quot == 0) {
					quot = '\'';
				}
			} else if (str.codePointAt(i) == ' ') {
				if (quot == 0) {
					ret.add(str.substring(s, i));
					s = i + " ".length();
				}
			}
			i = str.offsetByCodePoints(i, 1);
		}

		if (0 < i && quot == 0) {
			ret.add(str.substring(s, i));
		}

		return ret.toArray(new String[ret.size()]);
	}

	/**
	 * エスケープ解除変換
	 *
	 * @param str 変換対象文字列
	 * @return エスケープ解除文字列
	 */
	private static String unescape(final String str) {
		for (int i = 0; i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			if (str.codePointAt(i) == '=') {
				int next = i + "=".length();
				return str.substring(0, next) + strip(str.substring(next));
			} else if (str.codePointAt(i) == '\'') {
				return strip(str);
			} else if (str.codePointAt(i) == '"') {
				return strip(str);
			}
		}
		return str;
	}

	/**
	 * エスケープ変換
	 *
	 * @param str 変換対象文字列
	 * @return エスケープ文字列
	 */
	private static String escape(final String str) {
		for (int i = 0; i < str.length(); i = str.offsetByCodePoints(i, 1)) {
			if (str.codePointAt(i) == '=') {
				int next = i + "=".length();
				return str.substring(0, next) + toParamValue(str.substring(next));
			} else if (str.codePointAt(i) == '\'') {
				return toParamValue(str);
			} else if (str.codePointAt(i) == '"') {
				return toParamValue(str);
			}
		}
		return str;
	}

	/**
	 * パラメタ文字列化
	 * @param value 値
	 * @return パラメタ文字列
	 */
	private static String toParamValue(final String value) {
		String val = Objects.toString(value, "").trim();
		if (val.contains(" ")) {
			if (val.contains("\"")) {
				if (val.contains("'")) {
					throw new IllegalArgumentException(value);
				}
				return "'" + val + "'";
			}
			return "\"" + val + "\"";
		}
		return val;
	}

	/**
	 * パラメタ文字列をMapに変換する。
	 * @param params パラメタ文字列（Key=Valueの空白区切り）配列
	 * @return マップ
	 */
	public static Map<String, String[]> toMap(final String... params) {
		Map<String, String[]> map = new HashMap<>();
		if (params != null) {
			for (final String param : params) {
				int s = 0;
				for (int e = param.indexOf('='); 0 <= e; e = param.indexOf('=', s)) {
					String key = param.substring(s, e);
					String value = strip(getValue(param, e + "=".length()));
					addMap(map, key, value);
					s += key.length() + "=".length() + value.length();
					s += spaces(param, s);
				}
			}
		}
		return map;
	}

	/**
	 * マップに追加する。
	 * @param map マップ
	 * @param key キー
	 * @param vals 値
	 */
	private static void addMap(final Map<String, String[]> map,
					final String key, final String... vals) {
		String[] value = (vals == null) ? new String[0] : vals;
		Object obj = map.get(key);
		if (obj != null) {
			int lenA = Array.getLength(obj);
			int lenB = Array.getLength(value);
			String[] array = (String[]) Array.newInstance(String.class, lenA + lenB);
			System.arraycopy(obj, 0, array, 0, lenA);
			System.arraycopy(value, 0, array, lenA, lenB);
			map.put(key, array);
		} else {
			map.put(key, value);
		}
	}

	/**
	 * 値を取得する。
	 * @param param パラメタ文字列
	 * @param start 開始位置
	 * @return 値（クォーテーション付きの場合はそのまま）
	 */
	private static String getValue(final String param, final int start) {
		int end = start;
		String search = " ";
		if (start < param.length()) {
			if (param.codePointAt(start) == '"') {
				search = "\"";
				end += search.length();
			} else if (param.codePointAt(start) == '\'') {
				search = "'";
				end += search.length();
			}

			end = param.indexOf(search, end);
			if (end < 0) {
				end = param.length();
			} else if (!" ".equals(search)) {
				end += search.length();
			}
		}
		return param.substring(start, end);
	}

	/**
	 * クォーテーション削除
	 * @param str 対象文字列
	 * @return 削除後文字列
	 */
	private static String strip(final String str) {
		if (str != null) {
			String s = str.trim();
			if (2 <= s.length()) {
				if ((s.startsWith("\"") && s.endsWith("\""))
								|| (s.startsWith("'") && s.endsWith("'"))) {
					return s.substring(1, s.length() - 1);
				}
			}
		}
		return str;
	}

	/**
	 * スペース以外が存在するまでのスペースの数を返す。
	 * @param param パラメタ文字列
	 * @param start 開始位置
	 * @return スペース数
	 */
	private static int spaces(final String param, final int start) {
		int ret = 0;
		while (start + ret < param.length() && param.codePointAt(start + ret) == ' ') {
			ret += " ".length();
		}
		return ret;
	}

	/**
	 * パラメタマップをマージする。同一項目名は、値の配列に付加される。
	 * @param map1 パラメタマップ1
	 * @param map2 パラメタマップ2
	 * @return マージされたパラメタマップ
	 */
	public static Map<String, String[]> merge(
					final Map<String, String[]> map1, final Map<String, String[]> map2) {
		Map<String, String[]> ret = new HashMap<>();
		if (map1 != null) {
			ret.putAll(map1);
		}
		if (map2 != null) {
			for (final Entry<String, String[]> me : map2.entrySet()) {
				addMap(ret, me.getKey(), me.getValue());
			}
		}
		return ret;
	}

	/**
	 * パラメタ文字列化
	 * @param key キー
	 * @param values 値配列
	 * @return パラメタ文字列（Key=Valueの空白区切り）
	 */
	private static String toKeyValue(final String key, final String... values) {
		return values == null ? "" : Stream.of(values).
				map(v -> key + "=" + toParamValue(v)).collect(Collectors.joining(" "));
	}

	/**
	 * パラメタ文字列化
	 *
	 * @param strs Key=Value文字列配列
	 * @return パラメタ文字列（Key=Valueの空白区切り）
	 */
	public static String toParameter(final String... strs) {
		StringJoiner sj = new StringJoiner(" ");
		if (strs != null) {
			for (final String str : strs) {
				String s = Objects.toString(str, "").trim();
				if (!s.isEmpty()) {
					sj.add(escape(s));
				}
			}
		}
		return sj.toString();
	}

	/**
	 * オブジェクトからパラメタ文字列を作成します。
	 *
	 * @param obj オブジェクト
	 * @return パラメタ文字列（Key=Valueの空白区切り）
	 */
	public static String toParameter(final Object obj) {

		StringJoiner ret = new StringJoiner(" ");
		if (obj != null) {
			for (final Method mt : obj.getClass().getMethods()) {
				if (!mt.getName().startsWith("set")) {
					continue;
				}

				String mname = Factory.toItemName(mt);
				Method getter = Factory.getMethod(obj.getClass(), "get" + mname);
				Object val = Factory.invoke(obj, getter);
				if (val == null) {
					continue;
				}

				if (val.getClass().isArray()) {
					String decap = decapitalize(mname);
					for (Object o : Object[].class.cast(val)) {
						ret.add(toKeyValue(decap, toObjectString(o)));
					}
				} else {
					ret.add(toKeyValue(decapitalize(mname), toObjectString(val)));
				}
			}
		}
		return ret.toString();
	}

	/**
	 * オブジェクト文字列取得
	 *
	 * @param obj オブジェクト
	 * @return オブジェクト文字列
	 */
	private static String toObjectString(final Object obj) {
		if (Date.class.isInstance(obj)) {
			return DateUtil.toString(Date.class.cast(obj));
		}
		return Objects.toString(obj, "");
	}

	/**
	 * 項目名変換
	 *
	 * @param val 元項目名
	 * @return 変換後項目名
	 */
	private static String decapitalize(final String val) {
		int loc = val.offsetByCodePoints(0, 1);
		return val.substring(0, loc).toLowerCase(Locale.ENGLISH) + val.substring(loc);
	}
}
